﻿/*
 * Greenshot - a free and open source screenshot tool
 * Copyright (C) 2007-2013  Thomas Braun, Jens Klingen, Robin Krom
 *
 * For more information see: http://getgreenshot.org/
 * The Greenshot project is hosted on Sourceforge: http://sourceforge.net/projects/greenshot/
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

using GreenshotPlugin;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;

namespace Greenshot.IniFile
{
    public class IniConfig
    {
        private const string INI_EXTENSION = ".ini";
        private const string DEFAULTS_POSTFIX = "-defaults";
        private const string FIXED_POSTFIX = "-fixed";

        /// <summary>
        /// A lock object for the ini file saving
        /// </summary>
        private static object iniLock = new object();

        /// <summary>
        /// As the ini implementation is kept someone generic, for reusing, this holds the name of the application
        /// </summary>
        private static string applicationName = null;

        /// <summary>
        /// As the ini implementation is kept someone generic, for reusing, this holds the name of the configuration
        /// </summary>
        private static string configName = null;

        private static string configFolderPath = null;

        /// <summary>
        /// A Dictionary with all the sections stored by section name
        /// </summary>
        private static Dictionary<string, IniSection> sectionMap = new Dictionary<string, IniSection>();

        /// <summary>
        /// A Dictionary with the properties for a section stored by section name
        /// </summary>
        private static Dictionary<string, Dictionary<string, string>> sections = new Dictionary<string, Dictionary<string, string>>();

        /// <summary>
        /// A Dictionary with the fixed-properties for a section stored by section name
        /// </summary>
        private static Dictionary<string, Dictionary<string, string>> fixedProperties = null;

        /// <summary>
        /// Is the configuration portable (meaning we don't store it in the AppData directory)
        /// </summary>
        private static bool portable = false;
        public static bool IsPortable
        {
            get
            {
                return portable;
            }
        }

        public static bool AllowSave = true;

        /// <summary>
        /// Checks if we initialized the ini
        /// </summary>
        public static bool isInitialized
        {
            get
            {
                return applicationName != null && configName != null && sectionMap.Count > 0;
            }
        }

        /// <summary>
        /// Initialize the ini config
        /// </summary>
        /// <param name="applicationName"></param>
        /// <param name="configName"></param>
        public static void Init(string appName, string confName, string configFolder)
        {
            applicationName = appName;
            configName = confName;
            configFolderPath = configFolder;
            if (AllowSave)
            {
                Reload();
                WatchConfigFile(true);
            }
        }

        /// <summary>
        /// Default init
        /// </summary>
        public static void Init(string configFolder)
        {
            AssemblyProductAttribute[] assemblyProductAttributes = Assembly.GetEntryAssembly().GetCustomAttributes(typeof(AssemblyProductAttribute), false) as AssemblyProductAttribute[];
            if (assemblyProductAttributes.Length > 0)
            {
                string productName = assemblyProductAttributes[0].Product;
                LOG.InfoFormat("Using ProductName {0}", productName);
                Init(productName, "GreenshotImageEditor", configFolder);
            }
            else
            {
                throw new InvalidOperationException("Assembly ProductName not set.");
            }
        }

        /// <summary>
        /// Enable watching the configuration
        /// </summary>
        /// <param name="sendEvents"></param>
        private static void WatchConfigFile(bool sendEvents)
        {
            /*string iniLocation = CreateIniLocation(configName + INI_EXTENSION);
            // Wait with watching until the file is there
            if (Directory.Exists(Path.GetDirectoryName(iniLocation)))
            {
                if (watcher == null)
                {
                    //LOG.DebugFormat("Starting FileSystemWatcher for {0}", iniLocation);
                    // Monitor the ini file
                    watcher = new FileSystemWatcher();
                    watcher.Path = Path.GetDirectoryName(iniLocation);
                    watcher.Filter = "greenshot.ini";
                    watcher.NotifyFilter = NotifyFilters.LastWrite;
                    watcher.Changed += new FileSystemEventHandler(ConfigFileChanged);
                }
            }
            if (watcher != null)
            {
                watcher.EnableRaisingEvents = sendEvents;
            }*/
        }

        /// <summary>
        /// Called by the filesystem watcher when a file is changed.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="e"></param>
        private static void ConfigFileChanged(object source, FileSystemEventArgs e)
        {
            /*string iniLocation = CreateIniLocation(configName + INI_EXTENSION);
            if (iniLocation.Equals(e.FullPath))
            {
                LOG.InfoFormat("Config file {0} was changed, reloading", e.FullPath);

                // Try to reread the configuration
                int retries = 10;
                bool configRead = false;
                while (!configRead && retries != 0)
                {
                    try
                    {
                        IniConfig.Reload();
                        configRead = true;
                    }
                    catch (IOException)
                    {
                        retries--;
                        Thread.Sleep(100);
                    }
                }

                if (configRead && IniChanged != null)
                {
                    IniChanged.Invoke(source, e);
                }
            }*/
        }

        /// <summary>
        /// Create the location of the configuration file
        /// </summary>
        private static string CreateIniLocation(string configFilename)
        {
            if (applicationName == null || configName == null)
            {
                throw new InvalidOperationException("IniConfig.Init not called!");
            }

            /*string iniFilePath = null;
            string applicationStartupPath = "";
            try
            {
                applicationStartupPath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
            }
            catch (Exception exception)
            {
                LOG.WarnFormat("Problem retrieving the AssemblyLocation: {0} (Designer mode?)", exception.Message);
                applicationStartupPath = @".";
            }

            string pafPath = Path.Combine(applicationStartupPath, @"App\" + applicationName);

            if (portable || !portableCheckMade)
            {
                if (!portable)
                {
                    LOG.Info("Checking for portable mode.");
                    portableCheckMade = true;
                    if (Directory.Exists(pafPath))
                    {
                        portable = true;
                        LOG.Info("Portable mode active!");
                    }
                }
                if (portable)
                {
                    string pafConfigPath = Path.Combine(applicationStartupPath, @"Data\Settings");
                    try
                    {
                        if (!Directory.Exists(pafConfigPath))
                        {
                            Directory.CreateDirectory(pafConfigPath);
                        }
                        iniFilePath = Path.Combine(pafConfigPath, configFilename);
                    }
                    catch (Exception e)
                    {
                        LOG.InfoFormat("Portable mode NOT possible, couldn't create directory '{0}'! Reason: {1}", pafConfigPath, e.Message);
                    }
                }
            }

            if (iniFilePath == null)
            {
                // check if file is in the same location as started from, if this is the case
                // we will use this file instead of the Applicationdate folder
                // Done for Feature Request #2741508
                iniFilePath = Path.Combine(applicationStartupPath, configFilename);
                if (!File.Exists(iniFilePath))
                {
                    string iniDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), applicationName);
                    if (!Directory.Exists(iniDirectory))
                    {
                        Directory.CreateDirectory(iniDirectory);
                    }
                    iniFilePath = Path.Combine(iniDirectory, configFilename);
                }
            }
            LOG.InfoFormat("Using ini file {0}", iniFilePath);

            return iniFilePath;*/

            if (AllowSave)
            {
                if (!Directory.Exists(configFolderPath))
                {
                    Directory.CreateDirectory(configFolderPath);
                }
                return Path.Combine(configFolderPath, configFilename);
            }

            return null;
        }

        /// <summary>
        /// Reload the Ini file
        /// </summary>
        public static void Reload()
        {
            // Clear the current properties
            sections = new Dictionary<string, Dictionary<string, string>>();
            // Load the defaults
            Read(CreateIniLocation(configName + DEFAULTS_POSTFIX + INI_EXTENSION));
            // Load the normal
            Read(CreateIniLocation(configName + INI_EXTENSION));
            // Load the fixed settings
            fixedProperties = Read(CreateIniLocation(configName + FIXED_POSTFIX + INI_EXTENSION));

            foreach (IniSection section in sectionMap.Values)
            {
                try
                {
                    section.Fill(PropertiesForSection(section));
                    FixProperties(section);
                }
                catch (Exception ex)
                {
                    string sectionName = "unknown";
                    if (section != null && section.IniSectionAttribute != null && section.IniSectionAttribute.Name != null)
                    {
                        sectionName = section.IniSectionAttribute.Name;
                    }
                    LOG.WarnFormat("Problem reading the ini section {0}", sectionName);
                    LOG.Warn("Exception", ex);
                }
            }
        }

        /// <summary>
        /// This "fixes" the properties of the section, meaning any properties in the fixed file can't be changed.
        /// </summary>
        /// <param name="section">IniSection</param>
        private static void FixProperties(IniSection section)
        {
            // Make properties unchangable
            if (fixedProperties != null)
            {
                Dictionary<string, string> fixedPropertiesForSection = null;
                if (fixedProperties.TryGetValue(section.IniSectionAttribute.Name, out fixedPropertiesForSection))
                {
                    foreach (string fixedPropertyKey in fixedPropertiesForSection.Keys)
                    {
                        if (section.Values.ContainsKey(fixedPropertyKey))
                        {
                            section.Values[fixedPropertyKey].IsFixed = true;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Read the ini file into the Dictionary
        /// </summary>
        /// <param name="iniLocation">Path & Filename of ini file</param>
        private static Dictionary<string, Dictionary<string, string>> Read(string iniLocation)
        {
            if (!File.Exists(iniLocation))
            {
                LOG.Info("Can't find file: " + iniLocation);
                return null;
            }
            LOG.InfoFormat("Loading ini-file: {0}", iniLocation);
            //LOG.Info("Reading ini-properties from file: " + iniLocation);
            Dictionary<string, Dictionary<string, string>> newSections = IniReader.read(iniLocation, Encoding.UTF8);
            // Merge the newly loaded properties to the already available
            foreach (string section in newSections.Keys)
            {
                Dictionary<string, string> newProperties = newSections[section];
                if (!sections.ContainsKey(section))
                {
                    // This section is not yet loaded, simply add the complete section
                    sections.Add(section, newProperties);
                }
                else
                {
                    // Overwrite or add every property from the newly loaded section to the available one
                    Dictionary<string, string> currentProperties = sections[section];
                    foreach (string propertyName in newProperties.Keys)
                    {
                        string propertyValue = newProperties[propertyName];
                        if (currentProperties.ContainsKey(propertyName))
                        {
                            // Override current value as we are loading in a certain order which insures the default, current and fixed
                            currentProperties[propertyName] = propertyValue;
                        }
                        else
                        {
                            // Add "new" value
                            currentProperties.Add(propertyName, propertyValue);
                        }
                    }
                }
            }
            return newSections;
        }

        public static IEnumerable<string> IniSectionNames
        {
            get
            {
                foreach (string sectionName in sectionMap.Keys)
                {
                    yield return sectionName;
                }
            }
        }

        /// <summary>
        /// Method used for internal tricks...
        /// </summary>
        /// <param name="sectionName"></param>
        /// <returns></returns>
        public static IniSection GetIniSection(string sectionName)
        {
            IniSection returnValue = null;
            sectionMap.TryGetValue(sectionName, out returnValue);
            return returnValue;
        }

        /// <summary>
        /// A generic method which returns an instance of the supplied type, filled with it's configuration
        /// </summary>
        /// <returns>Filled instance of IniSection type which was supplied</returns>
        public static T GetIniSection<T>() where T : IniSection
        {
            T section;

            Type iniSectionType = typeof(T);
            string sectionName = IniSection.GetIniSectionAttribute(iniSectionType).Name;
            if (sectionMap.ContainsKey(sectionName))
            {
                //LOG.Debug("Returning pre-mapped section " + sectionName);
                section = (T)sectionMap[sectionName];
            }
            else
            {
                // Create instance of this type
                section = (T)Activator.CreateInstance(iniSectionType);

                // Store for later save & retrieval
                sectionMap.Add(sectionName, section);
                section.Fill(PropertiesForSection(section));
                FixProperties(section);
            }
            if (section.IsDirty)
            {
                LOG.DebugFormat("Section {0} is marked dirty, saving!", sectionName);
                Save();
            }
            return section;
        }

        /// <summary>
        /// Get the raw properties for a section
        /// </summary>
        /// <param name="section"></param>
        /// <returns></returns>
        public static Dictionary<string, string> PropertiesForSection(IniSection section)
        {
            Type iniSectionType = section.GetType();
            string sectionName = section.IniSectionAttribute.Name;
            // Get the properties for the section
            Dictionary<string, string> properties = null;
            if (sections.ContainsKey(sectionName))
            {
                properties = sections[sectionName];
            }
            else
            {
                sections.Add(sectionName, new Dictionary<string, string>());
                properties = sections[sectionName];
            }
            return properties;
        }

        /// <summary>
        /// Save the ini file
        /// </summary>
        public static void Save()
        {
            if (AllowSave)
            {
                bool acquiredLock = false;
                try
                {
                    acquiredLock = Monitor.TryEnter(iniLock, TimeSpan.FromMilliseconds(200));
                    if (acquiredLock)
                    {
                        // Code that accesses resources that are protected by the lock.
                        string iniLocation = CreateIniLocation(configName + INI_EXTENSION);
                        try
                        {
                            SaveInternally(iniLocation);
                        }
                        catch (Exception ex)
                        {
                            LOG.Error("A problem occured while writing the configuration file to: " + iniLocation);
                            LOG.Error(ex);
                        }
                    }
                    else
                    {
                        // Code to deal with the fact that the lock was not acquired.
                        LOG.Warn("A second thread tried to save the ini-file, we blocked as the first took too long.");
                    }
                }
                finally
                {
                    if (acquiredLock)
                    {
                        Monitor.Exit(iniLock);
                    }
                }
            }
        }

        /// <summary>
        /// The real save implementation, this first disables the watch otherwise we would be informed of our own changes.
        /// </summary>
        /// <param name="iniLocation"></param>
        private static void SaveInternally(string iniLocation)
        {
            WatchConfigFile(false);
            try
            {
                LOG.Info("Saving configuration to: " + iniLocation);
                if (!Directory.Exists(Path.GetDirectoryName(iniLocation)))
                {
                    Directory.CreateDirectory(Path.GetDirectoryName(iniLocation));
                }
                using (TextWriter writer = new StreamWriter(iniLocation, false, Encoding.UTF8))
                {
                    foreach (IniSection section in sectionMap.Values)
                    {
                        // Only save EditorConfiguration
                        if (section != null && section.IniSectionAttribute != null && section.IniSectionAttribute.Name == "Editor")
                        {
                            section.Write(writer, false);
                            // Add empty line after section
                            writer.WriteLine();
                            section.IsDirty = false;
                        }
                    }
                    writer.WriteLine();
                    // Write left over properties
                    foreach (string sectionName in sections.Keys)
                    {
                        // Check if the section is one that is "registered", if so skip it!
                        if (!sectionMap.ContainsKey(sectionName))
                        {
                            writer.WriteLine("; The section {0} hasn't been 'claimed' since the last start of Greenshot, therefor it doesn't have additional information here!", sectionName);
                            writer.WriteLine("; The reason could be that the section {0} just hasn't been used, a plugin has an error and can't claim it or maybe the whole section {0} is obsolete.", sectionName);
                            // Write section name
                            writer.WriteLine("[{0}]", sectionName);
                            Dictionary<string, string> properties = sections[sectionName];
                            // Loop and write properties
                            foreach (string propertyName in properties.Keys)
                            {
                                writer.WriteLine("{0}={1}", propertyName, properties[propertyName]);
                            }
                            writer.WriteLine();
                        }
                    }
                }
            }
            finally
            {
                try
                {
                    WatchConfigFile(true);
                }
                catch (Exception ex)
                {
                    LOG.Error("A problem occured while enabling the WatchConfigFile: ", ex);
                }
            }
        }
    }
}